使用stub破除external dependency

在测试中,如果因为代码对外部资源存在依赖的行为,尽管代码的逻辑是正确的也会存在测试失败的可能性,这也被称为test-inhibiting

  • 常见的external dependency是系统中的一个对象,被测试的代码需要与这个对象进行交互,但是你并不能去控制这个对象的行为。

  • stub则是对于系统的external dependency的可控制的替代物。stub带来的效果就是可以在测试代码中无需对external dependency进行直接处理。

提高代码的可测试性

破除dependency最直接的方式就是引入seam,当然这比如是需要refactoring配合的

A型方法:把具体类抽象成接口

  • 抽取接口以便对实现进行替换

B型方法:注入委托和接口(fake implementation)

  • 在被测试类注入stub
  • 在构造函数注入伪对象
  • 利用属性注入的方式注入伪对象
  • 在方法调用前注入问对象
抽取接口方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ExternalManager:IExternalManager
{
public bool IsExternal (string name)
{
......
}
}

public interface IExternalManager
{
bool IsExternal(string name);
}

//测试单元
public bool IsExternalManager(string name)
{
IexternalManager mgr = new ExternalManager();
return mgr.IsexternalManager(name);
}
  • 返回值为true的stub
1
2
3
4
5
6
7
public class AlwaysTrueExternalManager:IExternalManager
{
public bool IsExternal (string name)
{
return true;
}
}
在构造函数中注入伪对象

这种方式需要给测试类添加新的构造函数或者是个其构造函数添加新的参数,传入抽取出来的接口类型的对象,通过field或var 供被测试方法或其它相关区域的调用。

通过这种方式,测试需要先进行stub的配置,然后传入被测试对象。这种方式可以提高测试代码的可读性,将需要被了解的信息集中在一点地方。

同时,利用构造函数添加参数的方式实现注入,实际上也使得这些参数称为不可选的依赖项,也就是说类的使用者需要为每个所需的特定依赖传入参数。

利用构造函数注入伪对象也可能会带来一些问题,比如拥有多个external dependency 。这时候难道要添加多个构造函数或是添加多个参数?(明显是不明智的选择)

这个问题可以通过创建特殊的类,拥有可以初始化一个类所需要的所有值,这样构造函数就只需要拥有这个类作为唯一参数,当然更好的方式是通过IoC去实现。

属性注入

这个case需要为每个要注入的dependency添加一个属性,包括get和set。然后只需要在被测函数中相应的地方使用这些依赖。
与构造函数的依赖注入方式一样,需要指明必需的和可选的依赖项,因此也会影响到api的设计。通过使用属性,也同时说明了使用这个函数,那些依赖项是不需要的(可能这也是需要使用的属性注入的场景之一)。

方法调用前注入伪对象

这个case很明显的的意思就是我在对这个对象进行操作之前才可以得到其实例,而不是在构造函数或属性中就已经准备好,这个case的不同之处就是发起stub请求的对象就是被测试代码。

这样的话,可以通过使用工厂类来作为实例的提供者,当然必须在构造函数中初始化这个实例提供者。在这个工厂中,可以使用静态方法返回实例了接口的对象实例,同时采用别的方式(如扩展名)返回stub(这种方法会破坏类的设计封装)。

返回stub的层次

  • 第一层:在被测试类中伪造一个成员
    • 添加构造函数,在构造函数中设置类,从测试代码中设置构造参数,还要考虑对api的影响
    • 这种方式会改变被测试类的语义,在没有充分理由情况下一般不采用这种方式。
  • 第二层:在工厂类伪造一个成员

    • 上述方法调用前注入伪对象中提到的就是这一层,把工厂类的属性设置成伪依赖项,这种方式并不会改变语义,一切都是原样,代码也较为简洁。但是使用这种方式,必须要充分了解实例的使用时间。
  • 第三层:伪造工厂类

    • 这种方式需要实现自己专有的伪工厂类,同时这个类也能有或者可能没有接口,若没有,则需要为这个工厂类创建接口,然后再创建伪工厂实例,让其返回依赖项。这种层次可以理解为利用一个伪对象去返回一个伪对象,这个实在是很难想象的事情。

伪造方法

这种方法的层次,不同于上述几种,相比而言,其更接近于被测试代码(一般而言,约接近代码,越少需要更改依赖项)。很重要的一点就是这种方式将被测试的代码也作为一个依赖项。

  • 使用方式:

    • 在测试类中:
      * 添加返回真是实例的虚工厂方法
      * 在代码中正常的使用工厂方法
      
    • 在测试项目中:
      • 创建新的类
      • 声明这个新类继承被测试类
      • 创建需要替换的接口类型的公共字段(field)
      • 重写虚工厂方法
      • 返回公共字段
    • 测试代码中
      • 创建一个stub类的实例(实现相应接口)
      • 创建新的派生类而非被测试类的实例
      • 配置新实例的公共字段(设置成stub实例)

    在测试的派生类中,通过重写工厂方法,产品代码将使用配置的伪对象
    这种抽取和重写的方式,可以直接替换依赖项,实现方法避免了大量和接口和虚函数。

  • 这种方式比较适合用于模拟提供给被测试代码的输入,但是不适合验证被测试代码到依赖项的调用。
    如果被测试代码是web服务时,得到一个返回值,这是如果想要模拟自己的返回值的话,这种方法就很适合。但是如果想要测试代码对于web服务的调用时是否正确就显得捉襟见肘。
    当代码中以及存在可以伪造的接口的时候,有明显的可以使用seam的位置,便不需要使用这种方式。当然如果面对一个密封类,这种方法也显得很无力,无从下手。。。。。